# -*- coding:utf-8 -*-
import igraph as ig
from random import uniform
from math import sqrt
from itertools import combinations, repeat
import datetime
import time


class LayoutAlgorithmBase(object):
    def __init__(self, algorithm_name):
        self.name = algorithm_name


class LayoutAlgorithmIGraph(LayoutAlgorithmBase):

    def __init__(self, directed=False):
        super().__init__('igraph')
        self.directed = directed

    def execute(self, network):
        # get tuples of edges between nodes position, example:
        # [ (0,1) ]
        # before = datetime.datetime.now()
        before = time.time()
        nodes_for_graph = list(set(e["source"] for e in network['edges']) | set(e["target"] for e in network['edges']))
        edges_for_graph = [(nodes_for_graph.index(e['source']), nodes_for_graph.index(e['target'])) for e in network['edges']]
        print("node:  "+str(len(nodes_for_graph)))
        print("edge:  "+str(len(edges_for_graph)))

        assert(len(network["nodes"]) == len(nodes_for_graph))
        assert(len(network["edges"]) == len(edges_for_graph))
        G = ig.Graph(edges_for_graph, directed=False)
        layt = G.layout("kk", dim=3)
        layt.center()

        nodes = network['nodes']
        for i, dimention in enumerate(layt):
            index = nodes_for_graph[i]
            nodes[index]["location"] = [j*10 for j in dimention]
        # after = datetime.datetime.now()
        after = time.time()
        print(after-before)
        # print("time consume is:    "+str((after-before).seconds)+"seconds")
        # print("time consume is:    "+str((after-before).microseconds)+"microseconds")

        

    def execute_vad(self, network):
        nodes_for_graph = list(set(e["source"] for e in network['edges']) | set(e["target"] for e in network['edges']))
        edges_for_graph = [(nodes_for_graph.index(e['source']), nodes_for_graph.index(e['target'])) for e in network['edges']]

        assert(len(network["nodes"]) == len(nodes_for_graph))
        assert(len(network["edges"]) == len(edges_for_graph))

        rt = []
        rt.append(edges_for_graph[0][0])

        G = ig.Graph(edges_for_graph, directed=False)
        layt = G.layout_reingold_tilford(mode="in", root=rt)
        layt.center()

        nodes = network['nodes']
        for i, dimention in enumerate(layt):
            index = nodes_for_graph[i]
            nodes[index]["location"] = [j*200 for j in dimention]
            tmp = nodes[index]["location"][0]
            nodes[index]["location"][0] = nodes[index]["location"][1]
            nodes[index]["location"][1] = tmp


class LayoutAlgorithmFDL(LayoutAlgorithmBase):

    def __init__(self, directed=False):
        super().__init__('ForceDirectedLayout')
        self.directed = directed

    def execute(self, network, prev_nodes):
        curr_nodes = list(network["nodes"].keys())
        prev_nodes_list = list(prev_nodes.keys())
        new_nodes = list(set(curr_nodes).difference(set(prev_nodes_list)))
        if not new_nodes:
            for node in network["nodes"]:
                network["nodes"][node]["location"] = prev_nodes[node]["location"]
            return
        common_nodes = list(set(curr_nodes).intersection(set(prev_nodes_list)))
        stable_nodes = {}

        for cn in common_nodes:
            stable_nodes[cn] = {}
            stable_nodes[cn]["location"] = prev_nodes[cn]["location"]

        edges = network["edges"]
        nodes = network["nodes"]

        layout = LayoutAlgorithmFDL.iterate(edges, stable_nodes, new_nodes)
        for key in layout:  # 共同节点的位置和上一次保持不变
            nodes[key]["location"] = layout[key]["location"]

    @staticmethod
    def iterate(edges, stable_nodes, new_nodes, iterations=300, force_strength=5.0, dampening=0.01, max_velocity=2.0, max_distance=50):
        """
        iterations - 在生成节点的三维空间坐标过程中需要进行力导向布局的迭代次数
        force_strength - Strength of Coulomb and Hooke forces
                        (修改这个参数可以调整节点间的距离)
        dampening - Multiplier to reduce force applied to nodes
        max_velocity - 节点一次可以移动的最大距离
        max_distance - 考虑交互的最大距离
        """
        # 从给的边的信息中获得所有节点
        # nodes = set(e["source"] for e in edges) | set(e["target"] for e in edges)

        # 将nodes转化成一个字典并存储必要的数据并且初始化一些值
        d = 3
        nodes = {n: {"velocity": [0.0] * d, "force": [0.0] * d} for n in new_nodes}
        for key in stable_nodes:
            nodes[key] = {"velocity": stable_nodes[key]["location"], "force": [0.0] * d}

        # 只计算与新节点相连的边的引力
        new_edges = []
        for edge in edges:
            if edge["source"] in new_nodes or edge["target"] in new_nodes:
                new_edges.append(edge)

        # 重复n次,由iteration指定重复次数
        for _ in repeat(None, iterations):
            # 用库仑定律计算节点之间的两两斥力
            for node1, node2 in combinations(nodes.values(), 2):
                LayoutAlgorithmFDL._coulomb(node1, node2, force_strength, max_distance)

            # 用胡可定律计算节点之间的引力
            for edge in new_edges:
                LayoutAlgorithmFDL._hooke(nodes[edge["source"]], nodes[edge["target"]], force_strength, max_distance)

            # 计算总的受力并移动节点
            for node in nodes:
                # 将力约束到输入参数指定的边界
                force = [LayoutAlgorithmFDL._constrain(dampening * f, -max_velocity, max_velocity) for f in nodes[node]["force"]]
                # 更新velocity并重置受力
                nodes[node]["force"] = [0] * d
                if node in new_nodes:
                    nodes[node]["velocity"] = [v + dv for v, dv in zip(nodes[node]["velocity"], force)]

        # 清除无关参数并返回
        for node in nodes.values():
            del node["force"]
            node["location"] = node["velocity"]
            del node["velocity"]

        return nodes

    @staticmethod
    def _coulomb(n1, n2, k, r):
        """计算斥力并更新节点数据"""
        # 获取相关的位置数据
        delta = [x2 - x1 for x1, x2 in zip(n1["velocity"], n2["velocity"])]
        distance = sqrt(sum(d ** 2 for d in delta))

        # 如果增量太小，就使用随机值来保持移动
        if distance < 0.1:
            delta = [uniform(0.1, 0.2) for _ in repeat(None, 3)]
            distance = sqrt(sum(d ** 2 for d in delta))

        # 如果距离不大（即库仑可忽略不计），则计算
        if distance < r:
            force = (k / distance) ** 2
            n1["force"] = [f - force * d for f, d in zip(n1["force"], delta)]
            n2["force"] = [f + force * d for f, d in zip(n2["force"], delta)]

    @staticmethod
    def _hooke(n1, n2, k, r):
        """计算引力并更新节点数据"""
        # 获取相关的位置数据
        delta = [x2 - x1 for x1, x2 in zip(n1["velocity"], n2["velocity"])]
        distance = sqrt(sum(d ** 2 for d in delta))

        # 如果增量太小，就使用随机值来保持移动
        if distance < 0.1:
            delta = [uniform(0.1, 0.2) for _ in repeat(None, 3)]
            distance = sqrt(sum(d ** 2 for d in delta))

        # 调整距离来保证不会有过大的弹性
        distance = min(distance, r)

        # 计算胡可力并更新节点
        force = (distance ** 2 - k ** 2) / (distance * k)
        n1["force"] = [f + force * d for f, d in zip(n1["force"], delta)]
        n2["force"] = [f - force * d for f, d in zip(n2["force"], delta)]

    @staticmethod
    def _constrain(value, min_value, max_value):
        """约束输入范围的值"""
        return max(min_value, min(value, max_value))

